// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/touch_selection/longpress_drag_selector.h"

#include "base/auto_reset.h"
#include "ui/events/gesture_detection/motion_event.h"

namespace ui {
namespace {

    gfx::Vector2dF SafeNormalize(const gfx::Vector2dF& v)
    {
        return v.IsZero() ? v : ScaleVector2d(v, 1.f / v.Length());
    }

} // namespace

LongPressDragSelector::LongPressDragSelector(
    LongPressDragSelectorClient* client)
    : client_(client)
    , state_(INACTIVE)
    , has_longpress_drag_start_anchor_(false)
{
}

LongPressDragSelector::~LongPressDragSelector()
{
}

bool LongPressDragSelector::WillHandleTouchEvent(const MotionEvent& event)
{
    switch (event.GetAction()) {
    case MotionEvent::ACTION_DOWN:
        touch_down_position_.SetPoint(event.GetX(), event.GetY());
        touch_down_time_ = event.GetEventTime();
        has_longpress_drag_start_anchor_ = false;
        SetState(LONGPRESS_PENDING);
        return false;

    case MotionEvent::ACTION_UP:
    case MotionEvent::ACTION_CANCEL:
        SetState(INACTIVE);
        return false;

    case MotionEvent::ACTION_MOVE:
        break;

    default:
        return false;
    }

    if (state_ != DRAG_PENDING && state_ != DRAGGING)
        return false;

    gfx::PointF position(event.GetX(), event.GetY());
    if (state_ == DRAGGING) {
        gfx::PointF drag_position = position + longpress_drag_selection_offset_;
        client_->OnDragUpdate(*this, drag_position);
        return true;
    }

    // We can't use |touch_down_position_| as the offset anchor, as
    // showing the selection UI may have shifted the motion coordinates.
    if (!has_longpress_drag_start_anchor_) {
        has_longpress_drag_start_anchor_ = true;
        longpress_drag_start_anchor_ = position;
        return true;
    }

    // Allow an additional slop affordance after the longpress occurs.
    gfx::Vector2dF delta = position - longpress_drag_start_anchor_;
    if (client_->IsWithinTapSlop(delta))
        return true;

    gfx::PointF selection_start = client_->GetSelectionStart();
    gfx::PointF selection_end = client_->GetSelectionEnd();
    bool extend_selection_start = false;
    if (std::abs(delta.y()) > std::abs(delta.x())) {
        // If initial motion is up/down, extend the start/end selection bound.
        extend_selection_start = delta.y() < 0;
    } else {
        // Otherwise extend the selection bound toward which we're moving, or
        // the closest bound if motion is already away from both bounds.
        // Note that, for mixed RTL text, or for multiline selections triggered
        // by longpress, this may not pick the most suitable drag target
        gfx::Vector2dF start_delta = selection_start - longpress_drag_start_anchor_;
        gfx::Vector2dF end_delta = selection_end - longpress_drag_start_anchor_;

        // The vectors must be normalized to make dot product comparison meaningful.
        gfx::Vector2dF normalized_start_delta = SafeNormalize(start_delta);
        gfx::Vector2dF normalized_end_delta = SafeNormalize(end_delta);
        double start_dot_product = gfx::DotProduct(normalized_start_delta, delta);
        double end_dot_product = gfx::DotProduct(normalized_end_delta, delta);

        if (start_dot_product >= 0 || end_dot_product >= 0) {
            // The greater the dot product the more similar the direction.
            extend_selection_start = start_dot_product > end_dot_product;
        } else {
            // If we're already moving away from both endpoints, pick the closest.
            extend_selection_start = start_delta.LengthSquared() < end_delta.LengthSquared();
        }
    }

    gfx::PointF extent = extend_selection_start ? selection_start : selection_end;
    longpress_drag_selection_offset_ = extent - position;
    client_->OnDragBegin(*this, extent);
    SetState(DRAGGING);
    return true;
}

bool LongPressDragSelector::IsActive() const
{
    return state_ == DRAG_PENDING || state_ == DRAGGING;
}

void LongPressDragSelector::OnLongPressEvent(base::TimeTicks event_time,
    const gfx::PointF& position)
{
    // We have no guarantees that the current gesture stream is aligned with the
    // observed touch stream. We only know that the gesture sequence is downstream
    // from the touch sequence. Using a time/distance heuristic helps ensure that
    // the observed longpress corresponds to the active touch sequence.
    if (state_ == LONGPRESS_PENDING &&
        // Ensure the down event occurs *before* the longpress event. Use a
        // small time epsilon to account for floating point time conversion.
        (touch_down_time_ < event_time + base::TimeDelta::FromMicroseconds(10)) && client_->IsWithinTapSlop(touch_down_position_ - position)) {
        SetState(SELECTION_PENDING);
    }
}

void LongPressDragSelector::OnScrollBeginEvent()
{
    SetState(INACTIVE);
}

void LongPressDragSelector::OnSelectionActivated()
{
    if (state_ == SELECTION_PENDING)
        SetState(DRAG_PENDING);
}

void LongPressDragSelector::OnSelectionDeactivated()
{
    SetState(INACTIVE);
}

void LongPressDragSelector::SetState(SelectionState state)
{
    if (state_ == state)
        return;

    const bool was_dragging = state_ == DRAGGING;
    const bool was_active = IsActive();
    state_ = state;

    // TODO(jdduke): Add UMA for tracking relative longpress drag frequency.
    if (was_dragging)
        client_->OnDragEnd(*this);

    if (was_active != IsActive())
        client_->OnLongPressDragActiveStateChanged();
}

} // namespace ui
